package no.ffi.gismoidm.pubsub;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.security.PublicKey;
import java.util.Properties;
import java.util.UUID;
import javax.crypto.Cipher;
import javax.crypto.SealedObject;
import javax.crypto.SecretKey;
import no.ffi.gismoidm.idp.Config;
import no.ffi.gismoidm.invocations.ServiceInvocation;
import no.ffi.gismoidm.invocations.ServiceInvocationFactory;
import no.ffi.gismoidm.invocations.ServiceInvocationStateless;
import no.ffi.gismoidm.utils.IdentityStatement;
import no.ffi.gismoidm.utils.GismoIdmException;
import no.ffi.gismoidm.utils.SealedObjectReplacement;
import no.ffi.tpm.crypto.CryptoOperations;
import no.fongen.os.FIFO;

/**
 * Task of this class:
 * 1 - Receive incoming connections from Clients and MRs, authenticate them,
 * instantiate a neighbor object
 * 2 - Disconnect if the IS of the neighbor expires
 * 3 - Receive and validate new ISes
 * 4 - Send this node's IS to the neighbor it is connected to
 * 5 - Pass received messages to the neighbor object
 * 6 - Send outgoing messages on behalf of the node
 * 
 * The idea is to separate all code related to IdM operations and socket
 * communication within this class, whereas the Neighbor class manages
 * the PubSub- and ABAC-oriented operations.
 * 
 * @author anders
 */
public class Authenticator implements Runnable {

    Socket s;
    volatile ObjectInputStream ois;
    volatile ObjectOutputStream oos;
    IdentityStatement peerIS; // Peer's IS
    IdentityStatement myIS;
    MessageNeighbor peer;
    volatile SecretKey sessionKey;
    boolean incoming; // false if this an outward connection
    // Remember connection parameters in case of reconnection
    InetAddress endpoint;
    int port;
    String mrID;
    boolean sessionAuthenticated; // Flag for crude thread sync.

    FIFO outwardMessages;
    
    final static int refreshWhen=10; // Renew IDes 10 min before expiration

    public Authenticator(Socket s) {
        this.s = s;
        incoming = true;
        new Thread(this).start();
    }

    public Authenticator(InetAddress endpoint, int port, String mrID) {
        this.endpoint = endpoint;
        this.port = port;
        this.mrID = mrID;
        incoming = false;
        new Thread(this).start();
    }

    private void connectionSetup() {
        // Does not terminate until connection is made, so it should run
        // in a separate thread
        if (incoming) {
            // Do not connect an incoming connection, it has been connected
            // by the connection listener. Only create stream objects
            try {
                oos = new ObjectOutputStream(s.getOutputStream());
                ois = new ObjectInputStream(s.getInputStream());
            } catch (Exception e) {
                e.printStackTrace();
            }
            return;

        }
        // Code for outgoing calls
        boolean connected = false;
        while (!connected) {
            try {
                s = new Socket(endpoint, port);
                oos = new ObjectOutputStream(s.getOutputStream());
                ois = new ObjectInputStream(s.getInputStream());
                connected = true;
            } catch (IOException ioex) {
                System.out.println("Could not connect to " + mrID + " at "
                        + endpoint.getHostAddress() + ", port " + port);
                sleepFor(5); // TODO should be set to a higher value after testing
            }
        }
    }

    public void run() { // Thread for session authentication
        long clockCounter = 0; // This value is not used in stateless authentication
        int authProtocol = ServiceInvocationFactory.STATELESS;
        Object o;
        outwardMessages = new FIFO(20);
        MyCredentials.getInstance().addToIsDistribution(outwardMessages);
        CryptoOperations myCrOp = MyCredentials.getInstance().getCryptoOperator();

        while (true) {
            try {
                myIS = getIdentityStatement(); // May "never" return
                connectionSetup(); // Doesn't return  until connection is successful

                ServiceInvocationFactory sif = new ServiceInvocationFactory(authProtocol);
                // Read the request transaction for authentication

                if (incoming) {
                    // Start reading data from socket
                    o = ois.readObject(); // Exception here may indicate sync problems with sender

                    // Is this a ServiceInvocation object?
                    if (o instanceof ServiceInvocation) {
                        ServiceInvocation sii = (ServiceInvocation) o;
                        sii.validateRequest(myCrOp.getTrustAnchor(), clockCounter); // Throws exception if not approved
                        // Create an authentication respone
                        peerIS = sii.getIdentityStatement();
                        ServiceInvocation sio = sif.createResponseInstance("Response", peerIS, myIS, sii.getNonce(), myCrOp);
                        if (sio instanceof ServiceInvocationStateless) // Use the generated session key to encrypt subsequent traffic
                        {
                            sessionKey = ((ServiceInvocationStateless) sio).getSecretKey();
                        }
                        // Send response to peer
                        oos.writeObject(sio); // Don't use FIFO since we do not encrypt this message
                    }
                } else { // Outgoing connection
                    // Make a service invocation request
                    ServiceInvocation sio = sif.createRequestInstance("", myIS, myCrOp, clockCounter);
                    UUID requestUuid = sio.getNonce();
                    oos.writeObject(sio); // Send authentication request
                    Object f = ois.readObject(); // Await authentication response
                    if (f instanceof ServiceInvocation) {
                        ServiceInvocation sii = (ServiceInvocation) f;
                        sii.validateResponse(requestUuid, null, mrID, null, myCrOp);
                        peerIS = sii.getIdentityStatement();
                        sessionKey = ((ServiceInvocationStateless) sii).getSecretKey();
                    } else {
                        throw new GismoIdmException("Protocol error during authentication");
                    }
                }

                // Make ready for outoing traffic
                new MessageSender(outwardMessages);

                // We have an authenticated peer, now create a Neighbor object if the attribute PUBSUB_MR is present
                Properties subjectAttributes = peerIS.getAttributeAsProperties();
                if (subjectAttributes.containsKey("PUBSUB_MR")) { // Instantiate a MR object
                    peer = new MessageRouter(peerIS.getSubjectName(), peerIS, outwardMessages, myIS.getSubjectName());
                } else if (subjectAttributes.containsKey("PUBSUB_CLIENT")) { // Instantiate a Client object
                    peer = new MessageClient(peerIS.getSubjectName(), peerIS, outwardMessages, myIS.getSubjectName());
                } else {
                    System.out.println("Subject " + peerIS.getSubjectName() + " does not possess the "
                            + "required subject attributes");
                    s.close();
                    if (incoming) return;
                    sleepFor(30); // make another attempt in 30 sec
                }

                // Now start listening for message traffic
                sessionAuthenticated = true;
                while (true) {
                    o = ois.readObject();
                    System.out.println("------MESSAGE RECEIVED-------------");

                    // Is this encrypted, then decrypt it
                    if (o instanceof SealedObject) {
                        SealedObject so = (SealedObject) o;
                        o = so.getObject(sessionKey);
                    }
                    if (o instanceof IdentityStatement) { // new IS received, validate it
                        IdentityStatement newIS = (IdentityStatement) o;
                        System.out.println("Received an Identity Statement");
                        newIS.validateIdentityStatement(myCrOp.getTrustAnchor()); // Throws exception if invalid
                        // Check presence of subject attributes in new IS also
                        subjectAttributes = newIS.getAttributeAsProperties();
                        if (peer instanceof MessageRouter && !subjectAttributes.containsKey("PUBSUB_MR")) {
                            throw new IOException("Received Identity Statement does not authroize a Message Router");
                        } else if (peer instanceof MessageRouter && !subjectAttributes.containsKey("PUBSUB_CLIENT")) {
                            throw new IOException("Received Identity Statement does not authroize a Message Client");
                        }
                        if (newIS.validateSubjectName(peerIS.getSubjectName())) // Same subjectName as last time
                        {
                            peerIS = newIS;
                            peer.receiveMessage(peerIS); 
                        } else 
                            throw new GismoIdmException("Received Identity Statement has an invalid subjectname");
                    } else { // pass message to Neighbor object for processing
                        if (peerIS.identityStatementExpired(0)) { // IS is expired, terminate connection
                            throw new IOException("Expired Identity Statement");
                        }
                        peer.receiveMessage(o);
                    }
                }

            } catch (GismoIdmException pe) {
                pe.printStackTrace();
            } catch (IOException io) {
                //io.printStackTrace();
                if (incoming) { // No reconnect, terminate connection
                    System.out.println("IOException (" + io.getMessage() + ").. - terminating connection");
                    if (peer != null) peer.terminateClient();
                    //outwardMessages.nonblockput(new TerminateSignal()); Happens in peer.terminateClient
                    return;
                } // Else stay in while loop and reconnect
            } catch (ClassNotFoundException co) {
                co.printStackTrace();
            } catch (Exception ex) {
                ex.printStackTrace();
            }

        }//while
    }
    
    private void sleepFor(int seconds) {
        try { Thread.sleep(seconds*1000); }
        catch (Exception e) {}
    }

    
    /*
     * This class manages to outgoing traffic, obtaining new Identity statements when necessary
     */

    class MessageSender implements Runnable {

        FIFO outwardMessages;

        public MessageSender(FIFO outwardMessages) {
            this.outwardMessages = outwardMessages;
            new Thread(this).start();
        }

        public void run() {
            while (true) {
                try {
                    Object oo = outwardMessages.get(); // blocks on empty FIFO
                    // TODO The chosen design: MR checks recipient's IS before transmission, and
                    // the sender's IS at reception. Which means that "expired" MR's will still be
                    // able to receive messages. Replce this design with a separate thread to
                    // renew ISes, and check recipient's IS before sending a message.

                    // The only way to terminate this thread orderly is to let it react to a
                    // "terminate" object in the FIFO. If it receives a Terminate object, then it exits the thread
                    if (oo instanceof TerminateSignal) {
                        MyCredentials.getInstance().removeFromDistributionList(outwardMessages);
                        try {
                            s.close();
                        } catch (IOException e) { }                        
                        System.out.println("Sender thread now terminating");
                        return;
                    }
                    boolean sent = false;
                    while (!sent) { // Try to send, reconnect and retry if necessary
                        try {
                            if (peerIS.identityStatementExpired(0)) {
                                // Peer's identity statement is expired, so no
                                // message should be sent. Session is closed,
                                // but an attempt to reconnect is made
                                throw new SocketException();
                            }
                            // Encryption must be done for each attempt, since the
                            // crypto key may have changed in between
                            Cipher cip = Cipher.getInstance(Config.SYMMETRIC_CRYPTO_ALGORITHM);
                            cip.init(Cipher.ENCRYPT_MODE, sessionKey);
                            SealedObjectReplacement so = new SealedObjectReplacement((Serializable) oo, cip); // so is now AES encrypted.
                            oos.writeObject(so);
                            sent = true;
                        } catch (SocketException se) {
                            if (incoming) {
                                System.out.println("Error during sending over an incoming connection,"
                                        + " waiting for peer to reconnect. Outbound messages will be lost");
                                MyCredentials.getInstance().removeFromDistributionList(outwardMessages);

                                return; // end thread operation
                            } else {
                                System.out.println("Error during sending, attempting a reconnect");
                                //ois.close(); // Hopefully this triggers an exception in the reader thread
                                s.close();
                                sessionAuthenticated = false; // Very crude synchronization, but will have to do
                                System.out.println("Waiting for new session to authenticate");
                                while (!sessionAuthenticated) {
                                    sleepFor(5);
                                }
                                System.out.println("New session authenticated, now move on");
                            }
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private IdentityStatement getIdentityStatement() {
        IdentityStatement i = MyCredentials.getInstance().getIdentityStatement();
        while (i==null) {
            sleepFor(60);
            i = MyCredentials.getInstance().getIdentityStatement();
        }
        return i;
    }
    
}
